package org.wesc.boot.webapi.api.system;

import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.wesc.boot.common.annotation.ApiEndpoint;
import org.wesc.boot.common.api.LoginResultCode;
import org.wesc.boot.common.api.R;
import org.wesc.boot.common.utils.AesUtil;
import org.wesc.boot.common.utils.DateUtil;
import org.wesc.boot.common.utils.IpUtil;
import org.wesc.boot.common.utils.PasswordHelper;
import org.wesc.boot.dao.domain.ActiveUser;
import org.wesc.boot.dao.entity.Menu;
import org.wesc.boot.dao.entity.Role;
import org.wesc.boot.dao.entity.User;
import org.wesc.boot.dao.entity.UserConfig;
import org.wesc.boot.dao.model.UserResultModel;
import org.wesc.boot.dao.redis.RedisConstants;
import org.wesc.boot.dao.redis.RedisTemplateService;
import org.wesc.boot.secure.jwt.JwtToken;
import org.wesc.boot.secure.jwt.JwtUtil;
import org.wesc.boot.secure.properties.ShiroProperties;
import org.wesc.boot.service.system.loginlog.LoginLogQueryService;
import org.wesc.boot.service.system.menu.MenuAdvanceService;
import org.wesc.boot.service.system.user.UserAdvanceService;
import org.wesc.boot.service.system.user.UserLoginService;
import org.wesc.boot.webapi.base.BaseApi;

import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author Wesley
 */
@Api(tags = {"认证注销接口"})
@Slf4j
@RestController
@RequestMapping("/api")
public class LoginApi extends BaseApi {

    private static final double FUTURE_TIME = 9.9990817212102E13;

    private static final String USER_LOCKED = "1";

    @Value("${login.verifiedCode}")
    private boolean verifiedCode;

    private final ObjectMapper mapper;

    private final ShiroProperties shiroProperties;

    private final UserAdvanceService userAdvanceService;

    private final UserLoginService userLoginService;

    private final RedisTemplateService redisTemplateService;

    private final LoginLogQueryService loginLogQueryService;

    private final MenuAdvanceService menuAdvanceService;

    @Autowired
    public LoginApi(ObjectMapper mapper, ShiroProperties shiroProperties, UserAdvanceService userAdvanceService, UserLoginService userLoginService,
                    RedisTemplateService redisTemplateService, LoginLogQueryService loginLogQueryService, MenuAdvanceService menuAdvanceService) {
        this.mapper = mapper;
        this.shiroProperties = shiroProperties;
        this.userAdvanceService = userAdvanceService;
        this.userLoginService = userLoginService;
        this.redisTemplateService = redisTemplateService;
        this.loginLogQueryService = loginLogQueryService;
        this.menuAdvanceService = menuAdvanceService;
    }

    /**
     * 账户密码登录
     *
     * @return 是否需要验证码
     */
    @GetMapping("/login/showVerifiedCode")
    @ApiOperation(value = "是否显示登陆验证码")
    public R<Boolean> showVerifiedCode() {
        return R.data(verifiedCode);
    }

    /**
     * 账户密码登录
     *
     * @return loginWork
     */
    @PostMapping("/login")
    @ApiOperation(value = "用户登录")
    public R<Map<String, Object>> login(
            @RequestParam("account") String account,
            @RequestParam("password") String password,
            @RequestParam("rememberMe") boolean rememberMe,
            @RequestParam("loginType") String loginType,
            HttpServletRequest request) throws Exception {
        // 登陆类型校验
        if (null == loginType || !LoginType.checkLoginType(loginType)) {
            return R.fail(LoginResultCode.INVALID_LOGIN_TYPE);
        }

        // AES密码解密
        String aesDecryptedPwd = AesUtil.decrypt(password);

        UserResultModel userResultModel;
        if (loginType.equals(LoginType.LOGIN_ACCOUNT.getType())) {
            // 用户名
            userResultModel = userAdvanceService.findDetailByUsername(account);
        } else if (loginType.equals(LoginType.LOGIN_EMAIL.getType())) {
            // 邮箱
            userResultModel = userAdvanceService.findDetailByEmail(account);
        } else if (loginType.equals(LoginType.LOGIN_MOBILE.getType())) {
            // 手机号
            userResultModel = userAdvanceService.findDetailByMobile(account);
        } else {
            return R.fail(LoginResultCode.INVALID_LOGIN_TYPE);
        }

        if (null == userResultModel) {
            return R.fail(LoginResultCode.INVALID_USERNAME);
        }

        // 密码
        String encryptPassword = PasswordHelper.encryptPassword(aesDecryptedPwd);
        if (!StringUtils.equals(userResultModel.getPassword(), encryptPassword)) {
            return R.fail(LoginResultCode.PASSWORD_NOT_CORRECT);
        }

        return loginWork(userResultModel, request);
    }

    /**
     * 短信验证码登陆
     * @return loginWork
     */
    @PostMapping("/login/sms")
    public R<Map<String, Object>> smsLogin(
            @RequestParam("mobile") String mobile,
            @RequestParam("captcha") String captcha,
            @RequestParam("rememberMe") boolean rememberMe,
            HttpServletRequest request) throws Exception {
        // 校验手机号有效性
        UserResultModel userResultModel = userAdvanceService.findDetailByMobile(mobile);
        if (null == userResultModel) {
            return R.fail(LoginResultCode.INVALID_MOBILE);
        }

        // 短信验证码校验
        Object object = redisTemplateService.get(mobile);
        if (null == object) {
            // 验证码过期
            return R.fail(LoginResultCode.CAPTCHA_EXPIRED);
        } else {
            if (!captcha.equals(object.toString())) {
                // 验证不匹配
                return R.fail(LoginResultCode.CAPTCHA_INVALID);
            }
            return loginWork(userResultModel, request);
        }
    }

    /**
     * 登陆的后续工作
     * @param userResultModel 用户结果模型
     * @param request 请求体
     * @return 接口返回用户信息
     * @throws Exception 异常
     */
    private R<Map<String, Object>> loginWork(UserResultModel userResultModel, HttpServletRequest request) throws Exception {
        // 是否锁定
        if (userResultModel.getLocked().equals(USER_LOCKED)) {
            return R.fail(LoginResultCode.USER_LOCKED);
        }

        // 更新用户登录时间
        userLoginService.updateUserLoginTime(userResultModel);

        // 保存登录记录
        userLoginService.saveUserLoginLog(userResultModel);

        // JWT
        String token = JwtUtil.encryptToken(JwtUtil.signature(userResultModel.getUserName(), userResultModel.getPassword()));
        LocalDateTime expireTime = LocalDateTime.now().plusSeconds(shiroProperties.getJwtTimeOut());
        String expireTimeStr = DateUtil.formatFullTime(expireTime);
        JwtToken jwtToken = new JwtToken(token, expireTimeStr);

        // 存储token和在线用户信息于redis
        ActiveUser activeUser = this.saveTokenToRedis(userResultModel, jwtToken, request);
        userResultModel.setActiveId(activeUser.getActiveId());

        // 生成前端需要的用户信息
        Map<String, Object> userInfo = this.generateUserInfo(jwtToken, userResultModel);

        // 返回
        return R.success(LoginResultCode.LOGIN_SUCCESS, userInfo);
    }

    @ApiOperation(value = "用户首页")
    @GetMapping("/index/{username}")
    public R<Map<String, Object>> index(@NotBlank(message = "{required}") @PathVariable String username) {
        Map<String, Object> data = new HashMap<>(16);
        // 获取系统访问记录
        Long totalVisitCount = loginLogQueryService.findTotalVisitCount();
        data.put("totalVisitCount", totalVisitCount);
        Long todayVisitCount = loginLogQueryService.findTodayVisitCount();
        data.put("todayVisitCount", todayVisitCount);
        Long todayIp = loginLogQueryService.findTodayIp();
        data.put("todayIp", todayIp);

        // 获取近期系统访问记录
        List<Map<String, Object>> lastSevenVisitCount = loginLogQueryService.findLastSevenDaysVisitCount(null);
        data.put("lastSevenVisitCount", lastSevenVisitCount);
        User param = new User();
        param.setUserName(username);
        List<Map<String, Object>> lastSevenUserVisitCount = loginLogQueryService.findLastSevenDaysVisitCount(param);
        data.put("lastSevenUserVisitCount", lastSevenUserVisitCount);
        return R.data(data);
    }

    @ApiOperation(value = "踢出用户")
    @DeleteMapping("kickout/{id}")
    @RequiresPermissions("user:kickout")
    @ApiEndpoint(operation = "踢出用户", exception = "踢出用户失败")
    public R<String> kickout(@NotBlank(message = "{required}") @PathVariable String id) throws Exception {
        double now = System.currentTimeMillis();
        Set<String> onlineUserSet = redisTemplateService.zrangeByScore(RedisConstants.ACTIVE_USERS_ZSET_PREFIX, now, FUTURE_TIME);
        ActiveUser kickoutUser = null;
        String kickoutUserString = "";
        for (String onlineUser : onlineUserSet) {
            ActiveUser activeUser = mapper.readValue(onlineUser, ActiveUser.class);
            if (StringUtils.equals(activeUser.getActiveId(), id)) {
                kickoutUser = activeUser;
                kickoutUserString = onlineUser;
                break;
            }
        }
        if (kickoutUser != null && StringUtils.isNotBlank(kickoutUserString)) {
            // 删除 zset中的记录
            redisTemplateService.zrem(RedisConstants.ACTIVE_USERS_ZSET_PREFIX, kickoutUserString);
            // 删除对应的 token缓存
            redisTemplateService.del(RedisConstants.TOKEN_CACHE_PREFIX + kickoutUser.getToken() + "." + kickoutUser.getIp());
            // 删除此用户的短信验证码信息
            String mobile = userAdvanceService.findByUserId(kickoutUser.getUserId()).getMobile();
            if (StringUtils.isNotEmpty(mobile)) {
                redisTemplateService.del(mobile);
            }
        }
        return R.success();
    }

    @ApiOperation(value = "退出登录")
    @GetMapping("/logout/{id}")
    @ApiEndpoint(operation = "退出登录", exception = "退出登录失败")
    public R<?> logout(@NotBlank(message = "{required}") @PathVariable String id) throws Exception {
        this.kickout(id);
        return R.success();
    }

    @ApiOperation(value = "在线用户")
    @RequiresPermissions("user:online")
    @GetMapping("online")
    public R<List<ActiveUser>> userOnline(String username) throws Exception {
        double now = System.currentTimeMillis();
        Set<String> userOnlineStringSet = redisTemplateService.zrangeByScore(RedisConstants.ACTIVE_USERS_ZSET_PREFIX, now, FUTURE_TIME);
        List<ActiveUser> activeUsers = new ArrayList<>();
        for (String userOnlineString : userOnlineStringSet) {
            ActiveUser activeUser = mapper.readValue(userOnlineString, ActiveUser.class);
            activeUser.setToken(null);
            if (StringUtils.isNotBlank(username)) {
                if (StringUtils.equalsIgnoreCase(username, activeUser.getUserName())) {
                    activeUsers.add(activeUser);
                }
            } else {
                activeUsers.add(activeUser);
            }
        }
        return R.data(activeUsers);
    }

    /**
     * 保存token于redis
     *
     * @param user 用户
     * @param token 凭证
     * @param request 请求体
     * @return 活动用户
     * @throws Exception 异常
     */
    private ActiveUser saveTokenToRedis(User user, JwtToken token, HttpServletRequest request) throws Exception {
        String ip = IpUtil.getIpAddr(request);

        // 构建在线用户
        ActiveUser activeUser = new ActiveUser();
        activeUser.setUserId(user.getUserId());
        activeUser.setUserName(user.getUserName());
        activeUser.setIp(ip);
        activeUser.setToken(token.getToken());
        activeUser.setLocation(null);
        activeUser.setDevice(null);

        // zset 存储登录用户，score 为过期时间戳
        redisTemplateService.zadd(RedisConstants.ACTIVE_USERS_ZSET_PREFIX, mapper.writeValueAsString(activeUser), Double.parseDouble(token.getExpireTime()));
        log.info("score: " + Double.valueOf(token.getExpireTime()));
        // redis 中存储这个加密 token，key = 前缀 + 加密 token + .ip
        redisTemplateService.set(RedisConstants.TOKEN_CACHE_PREFIX + token.getToken() + StringPool.DOT + ip, token.getToken(), shiroProperties.getJwtTimeOut());

        return activeUser;
    }

    /**
     * 生成前端需要的用户信息，包括：
     * 1. token
     * 2. Vue Router
     * 3. 用户角色
     * 4. 用户权限
     * 5. 前端系统个性化配置信息
     *
     * @param token token
     * @param user  用户信息
     * @return UserInfo
     */
    private Map<String, Object> generateUserInfo(JwtToken token, UserResultModel user) {
        Map<String, Object> userInfo = new HashMap<>(16);
        userInfo.put("token", token.getToken());
        userInfo.put("expireTime", token.getExpireTime());

        Set<String> roles = userAdvanceService.queryUserRoles(user).stream().map(Role::getRoleName).collect(Collectors.toSet());
        userInfo.put("roles", roles);

        Set<String> menus = menuAdvanceService.findUserMenuPerms(user.getUserName()).stream().map(Menu::getPerms).collect(Collectors.toSet());
        userInfo.put("permissions", menus);

        UserConfig userConfig = userAdvanceService.queryUserConfig(user.getUserId());
        userInfo.put("config", userConfig);

        user.setPassword(null);
        userInfo.put("user", user);
        return userInfo;
    }

}
